梦想不会自己发光,真正闪耀的是那个为梦狂奔的你。献给知行的孩子们!(Eric.He著)
本文详细讲解三大基础交换类排序算法:选择排序、冒泡排序、插入排序,三者均为「原地排序」(空间复杂度极低,仅需常数级额外空间),核心思想简单、代码容易实现,是入门排序算法的必修课。三者的平均时间复杂度均为 O(n2),适合小规模数据排序
排序算法的核心作用:把一个「无序的数组 / 列表」,按照指定的规则(默认从小到大 / 升序),变成「有序的数组 / 列表」。
比如把 [5,2,9,1,5,6] 变成 [1,2,5,5,6,9],这个处理的过程,就是排序算法要做的事。
把数组元素从【最小】排到【最大】
比如:[5,2,9,1,5,6] → 升序排序后 → [1,2,5,5,6,9]
把数组元素从【最大】排到【最小】
比如:[5,2,9,1,5,6] → 降序排序后 → [9,6,5,5,2,1]
衡量一个算法在执行过程中,「运行快慢」的核心指标,它描述的是:当处理的数据量越多时,算法的执行时间,会以什么样的「速度」增长。
算法执行了多少次循环 / 操作。我们要学的冒泡、选择、插入排序,核心逻辑都是嵌套循环,而时间复杂度的本质,就是看「算法中执行次数最多的循环,会执行多少次」,排序算法的时间复杂度,几乎都由「循环次数」决定!
O(n)、O(n2),这个写法叫做大 O表示法,是时间复杂度的标准写法。
衡量一个算法在执行过程中,需要占用多少「内存空间」的核心指标。它描述的是:当处理的数据量(n)越多时,算法所需要的内存空间,会以什么样的速度增长。
算法额外申请了多少存储空间,不是「存储原始数据的空间」。比如排序一个数组arr,这个数组本身占用的内存是必须的,不算算法的空间开销,我们只看算法在执行时新申请的变量 / 数组 / 容器占用的空间。
O(n)、O(n2),这个写法叫做大 O表示法,是空间复杂度的标准写法(和时间复杂度写法一样)。
算法的两个核心评价指标:时间复杂度 + 空间复杂度
相邻的两个元素两两比较,如果顺序错了(前大后小)就交换位置;每一轮排序,都会把当前「最大的元素」像水里的气泡一样,慢慢 "浮" 到数组的最后面。
从小到大升序排序为目标,排序数组 [5,2,9,1,5,6],共 6 个元素,最多只需要排 5 轮(最后 1 个元素自然有序)
执行步骤:
#include <stdio.h>
// 交换函数
void swap(int arr[], int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
// 打印函数
void printArray(int arr[], int len) {
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
// 冒泡排序 升序
void bubbleSort(int arr[], int len) {
// 外层循环:控制排序轮数,n个元素最多n-1轮
for (int i = 0; i < len - 1; i++) {
// 内层循环:控制每轮比较次数,每轮减少i次(末尾i个元素已排序)
for (int j = 0; j < len - 1 - i; j++) {
// 相邻元素比较,前大后小则交换(升序)
if (arr[j] > arr[j+1]) {
swap(arr, j, j+1);
}
}
}
}
int main() {
int arr[] = {5, 2, 9, 1, 5, 6};
int len = sizeof(arr) / sizeof(arr[0]);
printf("冒泡排序前:");
printArray(arr, len);
bubbleSort(arr, len);
printf("冒泡排序后:");
printArray(arr, len);
return 0;
}
把数组分成「左边有序区」和「右边无序区」,每一轮都在「无序区」中找到「最小的元素」,然后把它和「无序区的第一个元素」交换位置。
从小到大升序排序为目标,排序数组 [5,2,9,1,5,6],共 6 个元素,最多排 5 轮
执行步骤:
#include <stdio.h>
void swap(int arr[], int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
void printArray(int arr[], int len) {
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
// 选择排序 升序
void selectionSort(int arr[], int len) {
// 外层循环:控制有序区间的末尾,n个元素最多n-1轮
for (int i = 0; i < len - 1; i++) {
int minIndex = i; // 假设无序区间第一个元素是最小值,记录其下标
// 内层循环:遍历无序区间,找到最小值的下标
for (int j = i + 1; j < len; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j; // 更新最小值下标
}
}
// 找到最小值后,和无序区间第一个元素交换
swap(arr, i, minIndex);
}
}
int main() {
int arr[] = {5, 2, 9, 1, 5, 6};
int len = sizeof(arr) / sizeof(arr[0]);
printf("选择排序前:");
printArray(arr, len);
selectionSort(arr, len);
printf("选择排序后:");
printArray(arr, len);
return 0;
}
把数组分成「左边有序区」和「右边无序区」,默认第一个元素是有序区;依次取出无序区的元素,在有序区里「找到合适的位置插入」,插完后有序区依然保持有序。
这个逻辑和我们打扑克牌时整理手牌一模一样:摸一张新牌,从后往前和手里的牌比,大的牌往后挪,找到空位就把新牌插进去,手里的牌永远是有序的。
从小到大升序排序为目标,排序数组 [5,2,9,1,5,6],共 6 个元素,最多排 5 轮
执行步骤:
#include <stdio.h>
void printArray(int arr[], int len) {
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
// 插入排序 升序
void insertionSort(int arr[], int len) {
// 外层循环:遍历无序区间,从下标1开始(下标0是初始有序区间)
for (int i = 1; i < len; i++) {
int insertVal = arr[i]; // 取出当前要插入的元素
int preIndex = i - 1; // 有序区间的最后一个元素下标
// 内层循环:向前找插入位置,有序元素 > 待插入元素 则后移
// preIndex >= 0 防止数组下标越界
while (preIndex >= 0 && arr[preIndex] > insertVal) {
arr[preIndex + 1] = arr[preIndex]; // 元素后移,腾出插入空间
preIndex--;
}
arr[preIndex + 1] = insertVal; // 找到位置,插入元素
}
}
int main() {
int arr[] = {5, 2, 9, 1, 5, 6};
int len = sizeof(arr) / sizeof(arr[0]);
printf("插入排序前:");
printArray(arr, len);
insertionSort(arr, len);
printf("插入排序后:");
printArray(arr, len);
return 0;
}
| 对比维度 | 插入排序 | 选择排序 | 冒泡排序 |
|---|---|---|---|
| 时间复杂度 | 全部O(n2) | 最好O(n)最坏O(n2) | 全部O(n2) |
| 空间复杂度 | O(1) | O(1) | O(1) |
| 稳定性 | 稳定 | 不稳定 | 稳定 |
| 核心特点 | 像理牌、移动为主、效率最高 | 找最值再交换、交换次数少 | 相邻交换、易理解、交换次数多 |
这三个排序的效率都是O(n2),看起来不高,但绝对不是没用的算法,它们的适用场景非常明确:
掌握这三个排序,你就打好了排序算法的基础,后续学习快速排序、归并排序等高级排序,会非常轻松!